﻿/*
   Copyright 2013 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using Flower.Client;
using Flower.Directory.Host.Default;
using Flower.Processing.Interruptions;
using Flower.Services;
using Flower.Workflow;
using Flower.Processing;

namespace Flower.Testing
{
    /// <summary>
    /// A workflow test case.
    /// </summary>
    /// <typeparam name="TProcess">Process definition type.</typeparam>
    public sealed class WorkflowTest<TProcess>
        where TProcess : Process, IProcessDefinition
    {
        private const string PROCESSOR_NAME = "TestProcessor";

        private readonly string _testName;

        private IDirectory _directory;
        private TProcess _process;
        private string _startActivity;
        private string _endActivity;
        private Action<IFlowerClient, TProcess> _checkOnFinish;
        private readonly Dictionary<string, Action<IFlowerClient, TProcess>> _afterChecks =
            new Dictionary<string, Action<IFlowerClient, TProcess>>();
        private readonly Dictionary<string, Action<IFlowerClient, TProcess>> _beforeChecks =
            new Dictionary<string, Action<IFlowerClient, TProcess>>();
        private readonly Dictionary<string, Action<IFlowerClient, TProcess>> _waitHandlers =
            new Dictionary<string, Action<IFlowerClient, TProcess>>();
        private string _activityToThrowException;
        private Type _expectedException;
        private Delegate _exceptionCheck;
        private string _pid;

        public WorkflowTest(string testName)
        {
            _testName = testName;
        }

        public WorkflowTest<TProcess> UsingDirectory(IDirectory directory)
        {
            _directory = directory;
            return this;
        }

        public WorkflowTest<TProcess> UsingMemoryDirectory(Func<DirectoryBuilder, DirectoryBuilder> build)
        {
            _directory = 
                build(new DirectoryBuilder(new MemoryDirectory.Directory(new ScriptEnginePool(1))))
                    .RegisterProcessor(PROCESSOR_NAME)
                    .Root("/Processors/" + PROCESSOR_NAME + "/Pending")
                        .Process(typeof(TProcess).AssemblyQualifiedName, out _pid).End()
                    .End()
                    .Build();

            return this;
        }

        public WorkflowTest<TProcess> Run(TProcess process)
        {
            _process = process;
            return this;
        }

        private static int GetActivityPosition(Processor processor, string activityName)
        {
            for (int i = 0; i < processor.Activities.Count; i++)
            {
                if (processor.Activities[i].Name == activityName)
                {
                    return i;
                }
            }

            return -1;
        }

        public WorkflowTest<TProcess> From(string activityName)
        {
            _startActivity = activityName;
            return this;
        }

        public WorkflowTest<TProcess> To(string activityName)
        {
            _endActivity = activityName;
            return this;
        }

        public WorkflowTest<TProcess> CheckBefore(string activityName, Action<IFlowerClient, TProcess> check)
        {
            _beforeChecks[activityName] = check;
            return this;
        }

        public WorkflowTest<TProcess> CheckAfter(string activityName, Action<IFlowerClient, TProcess> check)
        {
            _afterChecks[activityName] = check;
            return this;
        }

        public WorkflowTest<TProcess> CheckAfterFinish(Action<IFlowerClient, TProcess> check)
        {
            _checkOnFinish = check;
            return this;
        }

        public WorkflowTest<TProcess> OnWaitAt(string activityName, Action<IFlowerClient, TProcess> handler)
        {
            _waitHandlers[activityName] = handler;
            return this;
        }

        public WorkflowTest<TProcess> Expect<TException>(string activityName, Func<TException, bool> check)
            where TException: Exception
        {
            _activityToThrowException = activityName;
            _expectedException = typeof(TException);
            _exceptionCheck = check;
            return this;
        }

        private void MoveToProcessing(IFlowerClient client)
        {
            client.Directory.Move(_pid, string.Concat("/Processors/", PROCESSOR_NAME, "/Running"));
        }

        public void Go()
        {
            if (_directory == null) throw new InvalidOperationException("Directory has not been set.");
            if (_process == null) throw new InvalidOperationException("Process has not been set.");

            using (var client = new FlowerClient(_directory))
            {
                var processorSettings = new Processor.Settings 
                {
                    EmitBreakpoints = true,
                    StandardAdvisors = Processor.StandardAdvisors.Logging
                };

                var processor = new Processor(PROCESSOR_NAME, client, null, processorSettings);
                processor.Init(_pid, _process);

                int startPosition = (_startActivity != null) ? GetActivityPosition(processor, _startActivity) : 0;
                if (startPosition < 0)
                {
                    throw new InvalidOperationException(string.Format("Activity '{0}' doesn't exist.", _startActivity));
                }

                int endPosition = (_endActivity != null) ? GetActivityPosition(processor, _endActivity) : processor.Activities.Count;
                if (endPosition < 0)
                {
                    throw new InvalidOperationException(string.Format("Activity '{0}' doesn't exist.", _endActivity));
                }

                _process.__ActivityPointer = startPosition;

                MoveToProcessing(client);

                while (true)
                {
                    IActivity nextActivity = processor.NextActivity;

                    Action<IFlowerClient, TProcess> check;

                    if (_beforeChecks.TryGetValue(nextActivity.Name, out check))
                    {
                        check(client, _process);
                    }

                    Interruption interruption = processor.ExecuteActivity();

                    if 
                    (
                        _activityToThrowException == nextActivity.Name && 
                        (interruption == null || !(interruption is UnhandledException))
                    )
                    {
                        throw new WorkflowTestException
                        (
                            _testName,
                            "Activity hasn't thrown the exception expected.",
                            nextActivity,
                            null
                        );
                    }

                    if (interruption != null)
                    {
                        if (interruption is Finish)
                        {
                            if (endPosition != processor.Activities.Count)
                            {
                                throw new WorkflowTestException
                                (
                                    _testName,
                                    "Process has finished before reached the final activity of the test",
                                    nextActivity,
                                    null
                                );
                            }

                            if (_afterChecks.TryGetValue(nextActivity.Name, out check))
                            {
                                check(client, _process);
                            }

                            if (_checkOnFinish != null)
                            {
                                _checkOnFinish(client, _process);
                            }
                            
                            break;
                        }
                        else if (interruption is UnhandledException)
                        {
                            Exception exception = ((UnhandledException)interruption).Exception;

                            if 
                            (
                                !(
                                    _activityToThrowException == nextActivity.Name &&
                                    exception.GetType() == _expectedException &&
                                    (bool)_exceptionCheck.DynamicInvoke(exception)
                                )
                            )
                            {
                                throw new WorkflowTestException
                                (
                                    _testName,
                                    "Unhandled exception",
                                    nextActivity,
                                    ((UnhandledException)interruption).Exception
                                );
                            }
                            else
                            {
                                break;
                            }
                        }
                        else if (interruption is Waiting)
                        {
                            Action<IFlowerClient, TProcess> handler;

                            if (_waitHandlers.TryGetValue(nextActivity.Name, out handler))
                            {
                                handler(client, _process);
                                MoveToProcessing(client);
                            }
                            else
                            {
                                throw new WorkflowTestException
                                (
                                    _testName,
                                    "Unhandled waiting",
                                    nextActivity,
                                    null
                                );
                            }
                        }
                        else
                        {
                            if (_afterChecks.TryGetValue(nextActivity.Name, out check))
                            {
                                check(client, _process);
                            }
                        }
                    }
                    else
                    {
                        if (_afterChecks.TryGetValue(nextActivity.Name, out check))
                        {
                            check(client, _process);
                        }
                    }

                    if (_process.__ActivityPointer == endPosition)
                    {
                        if (_checkOnFinish != null)
                        {
                            _checkOnFinish(client, _process);
                        }

                        break;
                    }
                }
            }
        }
    }
}
